SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
7.8 KB · 165 lines tsx
Raw Blame History
1import { ExternalLink } from 'lucide-react';2import type { Metadata } from 'next';3import Link from 'next/link';4import { notFound } from 'next/navigation';5import { SensorTierBadge, SignificanceBadge, StatusBadge } from '@/components/ui/badges';6import { KV, Row } from '@/components/ui/key-value';7import { LiveAgo } from '@/components/ui/live';8import { Container, Empty, Note, Stat, StatGrid } from '@/components/ui/section';9import { api, ApiError, safe } from '@/lib/api';10import { fmtDateTime, fmtDuration, fmtInt, fmtScore } from '@/lib/format';11import { routes, SENSOR_TIER_LABELS, SURFACE_LABELS } from '@/lib/site';1213export const revalidate = 60;14export const metadata: Metadata = { title: 'Sensor', robots: { index: false } };1516export default async function SensorPage({ params }: { params: Promise<{ id: string }> }) {17  const { id } = await params;18  let s;19  try {20    s = await api.sensor(id);21  } catch (e) {22    if (e instanceof ApiError && e.notFound) notFound();23    throw e;24  }25  const [snaps, changes] = await Promise.all([safe(api.sensorSnapshots(id, 50)), safe(api.sensorChanges(id, 50))]);26  const stale = s.last_success_at && Date.now() - new Date(s.last_success_at).getTime() > 2 * 86_400_000;27  return (28    <Container>29      <div className="pb-4 pt-6 md:pt-10">30        <p className="eyebrow flex flex-wrap items-center gap-2">31          Sensor <StatusBadge status={s.status} /> <SensorTierBadge tier={s.tier} withLabel />32        </p>33        <h1 className="display mt-2 text-[24px] md:text-[32px]">34          <Link href={routes.company(s.company.slug)} className="hover:text-accent">35            {s.company.display_name}36          </Link>{' '}37          <span className="text-ink-3">·</span> {SURFACE_LABELS[s.surface] ?? s.surface}38        </h1>39        <p className="mt-2 text-sm">40          <a href={s.url} target="_blank" rel="noopener noreferrer" className="link mono inline-flex items-center gap-1 break-all text-xs">41            {s.url} <ExternalLink className="size-3 shrink-0" aria-hidden />42          </a>43        </p>44        {stale && (45          <p className="mt-2 text-sm text-warning" role="status">46            Last successfully checked <LiveAgo at={s.last_success_at} tick={30000} />47            {s.last_failure_class ? ` — recent failures: ${s.last_failure_class} (${s.consecutive_failures} consecutive)` : ''}.48          </p>49        )}50      </div>51      <StatGrid cols={6}>52        <Stat label="Observations" value={fmtInt(s.observation_count)} size="sm" />53        <Stat label="Snapshots" value={fmtInt(s.snapshot_count)} size="sm" />54        <Stat label="Changes" value={fmtInt(s.change_count)} hint={`${fmtInt(s.meaningful_change_count)} meaningful`} size="sm" />55        <Stat label="Events" value={fmtInt(s.event_count)} size="sm" />56        <Stat label="Quality" value={fmtScore(s.quality_score)} size="sm" />57        <Stat label="Interval" value={fmtDuration(s.current_interval_s)} hint={`tier ${s.tier} · ${SENSOR_TIER_LABELS[s.tier] ?? ''}`} size="sm" />58      </StatGrid>59      <div className="mt-6 grid gap-8 lg:grid-cols-12">60        <div className="space-y-8 lg:col-span-8">61          <section>62            <p className="eyebrow mb-2">Snapshot versions</p>63            {!snaps ? (64              <Empty title="Snapshots unavailable." />65            ) : snaps.items.length === 0 ? (66              <Empty>No snapshot stored yet for this sensor.</Empty>67            ) : (68              <div className="table-scroll">69                <table className="data-table compact">70                  <thead>71                    <tr>72                      <th className="num">v</th>73                      <th>Fetched</th>74                      <th>Title</th>75                      <th className="num">Blocks</th>76                      <th className="num">Text</th>77                      <th>Hash</th>78                      <th>Diff</th>79                    </tr>80                  </thead>81                  <tbody>82                    {snaps.items.map((v, i) => {83                      const prev = snaps.items[i + 1];84                      return (85                        <tr key={v.id}>86                          <td className="num tnum">{v.version_no}</td>87                          <td className="tnum text-xs">88                            <Link href={routes.snapshot(v.id)} className="link">89                              {fmtDateTime(v.fetched_at)}90                            </Link>91                          </td>92                          <td className="wrap text-ink-2">{v.title ?? '—'}</td>93                          <td className="num tnum">{fmtInt(v.block_count)}</td>94                          <td className="num tnum">{fmtInt(v.text_length)}</td>95                          <td className="mono text-[11px] text-ink-3">{v.content_hash.replace('sha256:', '').slice(0, 10)}</td>96                          <td className="text-xs">{prev ? <Link href={routes.snapshotDiff(prev.id, v.id)} className="link">vs v{prev.version_no}</Link> : <span className="text-ink-3">first</span>}</td>97                        </tr>98                      );99                    })}100                  </tbody>101                </table>102              </div>103            )}104          </section>105          <section>106            <p className="eyebrow mb-2">Detected changes</p>107            {!changes ? (108              <Empty title="Changes unavailable." />109            ) : changes.items.length === 0 ? (110              <Empty>No change detected yet.</Empty>111            ) : (112              <ul className="divide-y divide-rule border-y border-rule text-sm">113                {changes.items.map((c) => (114                  <li key={c.id} className="flex flex-wrap items-center gap-2 py-2">115                    <SignificanceBadge value={c.significance} />116                    <Link href={routes.change(c.id)} className="link">117                      {fmtDateTime(c.detected_at)}118                    </Link>119                    <span className="tnum text-xs text-ink-3">120                      +{c.blocks_added} · −{c.blocks_removed} · ~{c.blocks_modified}121                    </span>122                    <span className="mono ml-auto text-[11px] text-ink-3">{c.kind}</span>123                  </li>124                ))}125              </ul>126            )}127          </section>128        </div>129        <aside className="lg:col-span-4">130          <KV>131            <Row k="Connector">132              <span className="mono text-xs">{s.connector_id}</span>133            </Row>134            <Row k="Domain">135              <span className="mono text-xs">{s.domain}</span>136            </Row>137            <Row k="Discovery">138              {s.discovery_method ?? '—'} <span className="text-xs text-ink-3">· confidence {Math.round(s.discovery_confidence * 100)} %</span>139            </Row>140            <Row k="Last run">141              <LiveAgo at={s.last_run_at} tick={10000} />142            </Row>143            <Row k="Last success">144              <LiveAgo at={s.last_success_at} tick={10000} />145            </Row>146            <Row k="Last change">147              <LiveAgo at={s.last_change_at} tick={30000} />148            </Row>149            <Row k="Next run">{fmtDateTime(s.next_run_at)}</Row>150            <Row k="Last status">151              <span className="tnum">{s.last_status ?? '—'}</span>152              {s.last_failure_class && <span className="mono ml-2 text-xs text-danger">{s.last_failure_class}</span>}153            </Row>154            <Row k="Created">{fmtDateTime(s.created_at)}</Row>155            <Row k="Id">156              <span className="mono text-xs break-all">{s.id}</span>157            </Row>158          </KV>159          <Note className="mt-4">A sensor is a deployed connector instance attached to one public URL. It never bypasses authentication or challenges; when a page moves, the auto-repair loop tries redirects, sitemaps and navigation before flagging it for review.</Note>160        </aside>161      </div>162    </Container>163  );164}165